﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace Koopakiller.Numerics.FormulaSolver
{
    /// <summary>
    /// Provides methods and properties to solve multiple formulas.
    /// </summary>
    public sealed class FormulaCalculator
    {
        /// <summary>
        /// Set the instance counter to zero. It should only called when no instance exists.
        /// </summary>
        public static void ResetInstanceCounter()
        {
            InstanceCounter = 0;
        }
        internal static int InstanceCounter = 0;
        /// <summary>
        /// Gets the identification number of this instance.
        /// </summary>
        public int InstanceId { get; private set; }

        /// <summary>
        /// Verifies that two specified doubles are equal, or within the specified accuracy of each other. 
        /// </summary>
        /// <param name="d1">The first double to compare.</param>
        /// <param name="d2">The second double to compare.</param>
        /// <returns></returns>
        public bool IsEqual(double d1, double d2)
        {
            return d1 > d2 - this.ExactnessDelta && d1 < d2 + ExactnessDelta;
        }
        /// <summary>
        /// Gets or sets the allowed accuracy delta for <see cref="IsEqual"/>.
        /// </summary>
        public double ExactnessDelta { get; set; }

        //TODO: Plausibilitätsprüfung, d.h. jede Formel nochmal durchrechnen und die Ergebnisse überprüfen.

        /// <summary>
        /// Initializes a new instance of the <see cref="FormulaCalculator"/> class.
        /// </summary>
        public FormulaCalculator()
        {
            this.ExactnessDelta = 1e-10;
            this.Values = new Dictionary<string, double?>();
            this._Formulas = new List<Formula>();

            //? new instance > create new value
            ++InstanceCounter;
            this.InstanceId = InstanceCounter;
        }

        /// <summary>
        /// Create a List with predictable values from the current <see cref="Values"/> list an the <see cref="_Formulas"/> list.<para/>
        /// Set values are not deleted.
        /// </summary>
        public void CreateValueList()
        {
            this.Values = this._Formulas
                              .Select(x => x.ResultSymbol)
                              .Union(this.Values.Where(x => x.Value != null).Select(x => x.Key))//values + formulas
                              .Distinct()//find distinct
                              .ToDictionary(x => x, y => !this.Values.ContainsKey(y) ? (double?)null : this.Values[y]);//create new value pair
        }

        List<Formula> _Formulas = null;
        /// <summary>
        /// Gets a list with formulas.
        /// </summary>
        List<Formula> Formulas
        {
            get
            {
                return _Formulas;
            }
        }
        /// <summary>
        /// Adds a formula to the collection.
        /// </summary>
        /// <param name="formula">The formula to be added.</param>
        public void AddFormula(Formula formula)
        {
            this._Formulas.Add(formula);
        }

        /// <summary>
        /// Gets a list with values.
        /// </summary>
        public Dictionary<string, double?> Values { get; private set; }

        /// <summary>
        /// Try to calculate the maximum of values in <see cref="Values"/> with <see cref="Formulas"/>.
        /// </summary>
        public void Calculate()
        {
            bool flag = true;
            while (flag)//run while formula was calculated
            {
                flag = false;
                foreach (var formula in this._Formulas)//Alle Formeln durch gehen
                {
                    Debug.WriteLine("Parameters : " + string.Join(", ", formula.RequiredParameterSymbols));
                    if (this.Values.ContainsKey(formula.ResultSymbol) && this.Values[formula.ResultSymbol] == null) //? Wert soll Berechnet werden und wurde noch nicht berechnet
                    {
                        if (!formula.RequiredParameterSymbols.Any(x => !this.Values.ContainsKey(x) || this.Values[x] == null)) //? Values enthält alle nötigen Parameter
                        {
                            if (formula.CanCalculate == null || formula.CanCalculate(this.Values)) //? Keine Bedingung für die Formel angegeben o. Bedingung ist ok
                            {
                                var res = formula.Calculate(this.Values);
                                if (res.Count() == 1)
                                {
                                    //x this.Values = res.ElementAt(0);//Nicht benötigt, da Referenz
                                    OnValueCalculated(formula.Identifier, formula.ResultSymbol, this.Values);
                                    flag = true;//Schleife anschließend nochmals durchlaufen
                                    break;//Schleife verlassen, weil sowieso von vorne begonnen wird.
                                }
                                else if (res.Count() == 0)
                                {
                                    //Fehler > Fehlerbehandlung!?
                                }
                                else
                                {
                                    this.Values = res.ElementAt(0);

                                    //! Andere Elemente ebenfalls noch weiter verarbeiten
                                    using (var enu = res.GetEnumerator())
                                    {
                                        enu.MoveNext();//? 1. Element überspringen
                                        while (enu.MoveNext())
                                        {
                                            //Neue Instanz erstellen und übergeben.
                                            FormulaCalculator fsNew = new FormulaCalculator();
                                            fsNew._Formulas = this._Formulas;
                                            fsNew.Values = enu.Current;
                                            OnFormulaSplit(formula.Identifier, formula.ResultSymbol, fsNew);
                                        }
                                    }
                                    OnValueCalculated(formula.Identifier, formula.ResultSymbol, this.Values);

                                    flag = true;
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }

        private void OnValueCalculated(FormulaIdentifier identifier, string valueSymbol, Dictionary<string, double?> values)
        {
            var evt = this.ValueCalculated;
            if (evt != null)
                evt(this, new FormulaCalculatedEventArgs(identifier, valueSymbol, values));
        }

        /// <summary>
        /// Occurs when a formula is calculated.
        /// </summary>
        public event EventHandler<FormulaCalculatedEventArgs> ValueCalculated;

        private void OnFormulaSplit(FormulaIdentifier identifier, string valueSymbol, FormulaCalculator newValues)
        {
            var evt = this.FormulaSplit;
            if (evt != null)
                evt(this, new FormulaSplitEventArgs(identifier, valueSymbol, newValues));
        }

        /// <summary>
        /// Occurs when a formula return a second value.
        /// </summary>
        public event EventHandler<FormulaSplitEventArgs> FormulaSplit;
    }
}
